TextBox controls offer a natural way for users to enter a value in your program. For this reason, they tend to be the most frequently used controls in the majority of Windows applications. TextBox controls, which have a great many properties and events, are also among the most complex intrinsic controls. In this section, I guide you through the most useful properties of TextBox controls and show how to solve some of the problems that you're likely to encounter.
After you place a TextBox control on a form, you must set a few basic properties. The first thing I do as soon as I create a new TextBox control is clear its Textproperty. If this is a multiline field, I also set the MultiLineproperty to True.
You can set the Alignment property of TextBox controls to left align, right align, or center the contents of the control. Right-aligned TextBox controls are especially useful when you're displaying numeric values. But you should be aware of the following quirk: while this property always works correctly when the Multiline property is set to True, it works with single-line controls only under Microsoft Windows 98, Microsoft Windows NT 4 with Service Pack 3, or later versions. Under previous versions of Windows 9x or Windows NT, no error is raised but single-line TextBox controls ignore the Alignment property and always align their contents to the left.
You can prevent the user from changing the contents of a TextBox control by setting its Lockedproperty to True. You usually do this if the control contains the result of a calculation or displays a field taken from a database opened in read-only mode. In most cases, you can achieve the same result using a Label control with a border and white background, but a locked TextBox control also permits your users to copy the value to the Clipboard and scroll through it if it's too large for the field's width.
If you're dealing with a numeric field, you probably want to set a limit on the number of characters that the user can enter in the field. You can do it very easily using the MaxLength property. A 0 value (the default) means that you can enter any number of characters; any positive value N enforces a limit to the length of the field's contents to be N characters long.
If you're creating password fields, you should set the PasswordChar property to a character string, typically an asterisk. In this case, your program can read and modify the contents of this TextBox control as usual, but users see only a row of asterisks.
CAUTION
Password-protected TextBox controls effectively disable the Ctrl+X and Ctrl+C keyboard shortcuts, so malicious users can't steal a password entered by another user. If, however, your application includes an Edit menu with all the usual clipboard commands, it's up to you to disable the Copy and Cut commands when the focus is on a password-protected field.
You can set other properties for a better appearance of the control—the Font property, for example. In addition, you can set the ToolTipText property to help users understand what the TextBox control is for. You can also make borderless TextBox controls by setting their BorderStyle property to 0-None, but controls like these don't appear frequently in Windows applications. In general, you can't do much else with a TextBox control at design time. The most interesting things can be done only through code.
The Text property is the one you'll reference most often in code, and conveniently it's the default property for the TextBox control. Three other frequently used properties are these:
TIP
When you want to append text to a TextBox control, you should use the following code (instead of using the concatenation operator) to reduce flickering and improve performance:
Text1.SelStart = Len(Text1.Text) Text1.SelText = StringToBeAdded
One of the typical operations you could find yourself performing with these properties is selecting the entire contents of a TextBox control. You often do it when the caret enters the field so that the user can quickly override the existing value with a new one, or start editing it by pressing any arrow key:
Private Sub Text1_GotFocus() Text1.SelStart = 0 ' A very high value always does the trick. Text1.SelLength = 9999 End Sub |
Always set the SelStart property first and then the SelLength or SelText properties. When you assign a new value to the SelStart property, the other two are automatically reset to 0 and an empty string respectively, thus overriding your previous settings.
TextBox controls support KeyDown, KeyPress, and KeyUp standard events, which Chapter 2 covered. One thing that you will often do is prevent the user from entering invalid keys. A typical example of where this safeguard is needed is a numeric field, for which you need to filter out all nondigit keys:
Private Sub Text1_KeyPress(KeyAscii As Integer) Select Case KeyAscii Case Is < 32 ' Control keys are OK. Case 48 To 57 ' This is a digit. Case Else ' Reject any other key. KeyAscii = 0 End Select End Sub |
You should never reject keys whose ANSI code is less than 32, a group that includes important keys such as Backspace, Escape, Tab, and Enter. Also note that a few control keys will make your TextBox beep if it doesn't know what to do with them—for example, a single-line TextBox control doesn't know what to do with an Enter key.
CAUTION
Don't assume that the KeyPress event will trap all control keys under all conditions. For example, the KeyPress event can process the Enter key only if there's no CommandButton control on the form whose Default property is set to True. If the form has a default push button, the effect of pressing the Enter key is clicking on that button. Similarly, no Escape key goes through this event if there's a Cancel button on the form. Finally, the Tab control key is trapped by a KeyPress event only if there isn't any other control on the form whose TabStop property is True.
You can use the KeyDown event procedure to allow users to increase and decrease the current value using Up and Down arrow keys, as you see here:
Private Sub Text1_KeyDown(KeyCode As Integer, Shift As Integer) Select Case KeyCode Case vbKeyUp Text1.Text = CDbl(Text1.Text) + 1 Case vbKeyDown Text1.Text = CDbl(Text1.Text) -1 End Select End Sub |
NOTE
There's a bug in the implementation of TextBox ready-only controls. When the Locked property is set to True, the Ctrl+C key combination doesn't correctly copy the selected text to the Clipboard, and you must manually implement this capability by writing code in the KeyPress event procedure.
Although trapping invalid keys in the KeyPress or KeyDown event procedures seems a great idea at first, when you throw your application to inexperienced users you soon realize that there are many ways for them to enter invalid data. Depending on what you do with this data, your application can come to an abrupt end with a run-time error or—much worse—it can appear to work correctly while it delivers bogus results. What you really need is a bullet-proof method to trap invalid values.
Before I offer you a decent solution to the problem, let me explain why you can't rely solely on trapping invalid keys for your validation chores. What if the user pastes an invalid value from the clipboard? Well, you might say, let's trap the Ctrl+V and Shift+Ins key combinations to prevent the user from doing that! Unfortunately, Visual Basic's TextBox controls offer a default edit menu that lets users perform any clipboard operation by simply right-clicking on them. Fortunately, there's a way around this problem: Instead of trapping a key before it gets to the TextBox control, you trap its effect in the Change event and reject it if it doesn't pass your test. But this makes the structure of the code a little more complex than you might anticipate:
' Form-level variables Dim saveText As String Dim saveSelStart As Long Private Sub Text1_GotFocus() ' Save values when the control gets the focus. saveText = Text1.Text saveSelStart = Text1.SelStart End Sub Private Sub Text1_Change() ' Avoid nested calls. Static nestedCall As Boolean If nestedCall Then Exit Sub ' Test the control's value here. If IsNumeric(Text1.Text) Then ' If value is OK, save values. saveText = Text1.Text saveSelStart = Text1.SelStart Else ' Prepare to handle a nested call. nestedCall = True Text1.Text = saveText nestedCall = False Text1.SelStart = saveSelStart End If End Sub Private Sub Text1_KeyUp(KeyCode As Integer, Shift As Integer) saveSelStart = Text1.SelStart End Sub Private Sub Text1_MouseDown(Button As Integer, _ Shift As Integer, X As Single, Y As Single) saveSelStart = Text1.SelStart End Sub Private Sub Text1_MouseMove(Button As Integer, _ Shift As Integer, X As Single, Y As Single) saveSelStart = Text1.SelStart End Sub |
If the control's value doesn't pass your tests in the Change event procedure, you must restore its previous valid value; this action recursively fires a Change event, and you must prepare yourself to neutralize this nested call. You might wonder why you also need to trap the KeyUp, MouseDown, and MouseMove events: The reason is that you always need to keep track of the last valid position for the insertion point because the end user could move it using arrow keys or the mouse.
The preceding code snippet uses the IsNumeric function to trap invalid data. You should be aware that this function isn't robust enough for most real-world applications. For example, the IsNumeric function incorrectly considers these strings as valid numbers:
123,,,123 345- $1234 ' What if it isn't a currency field? 2.4E10 ' What if I don't want to support scientific notation? |
To cope with this issue, I have prepared an alternative function, which you can modify for your particular purposes. (For instance, you can add support for a currency symbol or the comma as the decimal separator.) Note that this function always returns True when it's passed a null string, so you might need to perform additional tests if the user isn't allowed to leave the field blank:
Function CheckNumeric(text As String, DecValue As Boolean) As Boolean Dim i As Integer For i = 1 To Len(text) Select Case Mid$(text, i, 1) Case "0" To "9" Case "-", "+" ' Minus/plus signs are only allowed as leading chars. If i > 1 Then Exit Function Case "." ' Exit if decimal values not allowed. If Not DecValue Then Exit Function ' Only one decimal separator is allowed. If InStr(text, ".") < i Then Exit Function Case Else ' Reject all other characters. Exit Function End Select Next CheckNumeric = True End Function |
If your TextBox controls are expected to contain other types of data, you might be tempted to reuse the same validation framework I showed you previously—including all the code in the GotFocus, Change, KeyUp, MouseDown, and MouseMove event procedures—and replace only the call to IsNumeric with a call to your custom validation routine. Things aren't as simple as they appear at first, however. Say that you have a date field: Can you use the IsDate function to validate it from within the Change event? The answer is, of course, no. In fact, as you enter the first digit of your date value, IsDate returns False and the routine therefore prevents you from entering the remaining characters, and so preventing you from entering any value.
This example explains why a key-level validation isn't always the best answer to your validation needs. For this reason, most Visual Basic programmers prefer to rely on field-level validation and test the values only when the user moves the input focus to another field in the form. I explain field-level validation in the next section.
Visual Basic 6 has finally come up with a solution for most of the validation issues that have afflicted Visual Basic developers for years. As you'll see in a moment, the Visual Basic 6 approach is simple and clean; it really astonishes me that it took six language versions to deliver such a lifesaver. The keys to the new validation features are the Validate event and the CausesValidation property. They work together as follows: When the input focus leaves a control, Visual Basic checks the CausesValidation property of the control that is about to receive the focus. If this property is True, Visual Basic fires the Validate event in the control that's about to lose the focus, thus giving the programmer a chance to validate its contents and, if necessary, cancel the focus shift.
Let's try a practical example. Imagine that you have five controls on a form: a required field (a TextBox control, txtRequired, that can't contain an empty string), a numeric field, txtNumeric, that expects a value in the range 1 through 1000, and three push buttons: OK, Cancel, and Help. (See Figure 3-1.) You don't want to perform validation if the user presses the Cancel or Help buttons, so you set their CausesValidation properties to False. The default value for this property is True, so you don't have to modify it for the other controls. Run the sample program on the companion CD, type something in the required TextBox, and then move to the second field. Because the second field's CausesValidation property is True, Visual Basic fires a Validate event in the first TextBox control:
Private Sub txtRequired_Validate(Cancel As Boolean) ' Check that field is not empty. If txtRequired.Text = "" Then MsgBox "Please enter something here", vbExclamation Cancel = True End If End Sub |
If the Cancel parameter is set to True, Visual Basic cancels the user's action and takes the input focus back on the txtRequired control: No other GotFocus and LostFocus events are generated. On the other hand, if you typed something in the required field, the focus will now be on the second field (the numeric text box). Try clicking on the Help or Cancel buttons: No Validate event will fire this time because you set the CausesValidation property for each of these controls to False. Instead, click on the OK button to execute the Validate event of the numeric field, where you can check it for invalid characters and valid range.
Figure 3-1. A demonstration program that lets you experiment with the new Visual Basic Validate features.
Private Sub txtNumeric_Validate(Cancel As Boolean) If Not IsNumeric(txtNumeric.Text) Then Cancel = True ElseIf CDbl(txtNumeric.Text) < 1 Or CDbl(txtNumeric.Text) > 1000 Then Cancel = True End If If Cancel Then MsgBox "Please enter a number in range [1-1000]", vbExclamation End If End Sub |
In some circumstances, you might want to programmatically validate the control that has the focus without waiting for the user to move the input focus. You can do it with the form's ValidateControls method, which forces the Validate event of the control that has the input focus. Typically, you do it when the user closes the form:
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer) ' You can't close this form without validating the current field. If UnloadMode = vbFormControlMenu Then On Error Resume Next ValidateControls If Err = 380 Then ' The current field failed validation. Cancel = True End If End If End Sub |
Checking the UnloadMode parameter is important; otherwise, your application will mistakenly execute a ValidateControls method when the user clicks on the Cancel button. Note that ValidateControls returns an error 380 if Cancel was set in the Validate event procedure of the control that had the focus.
CAUTION
Visual Basic 6's validation scheme has two flaws, though. If your form has a CommandButton whose Default property is set to True, pressing the Enter key while the input focus is on another control results in a click on the CommandButton control but doesn't fire a Validate event, even if the CausesValidation property of the CommandButton control is set to True. The only way to solve this problem is to invoke the ValidateControls method from within the default CommandButton control's Click event procedure.The second flaw is that the Validate event doesn't fire when you're moving the focus from a control whose CausesValidation property is False, even if the control that receives the focus has its CausesValidation property set to True.
The new Visual Basic 6 validation mechanism is simple and can be implemented with little effort. But it isn't the magic answer to all your validation needs. In fact, this technique can only enforce field-level validation; it does nothing for record-level validation. In other words, it ensures that one particular field is correct, not that all fields in the form contain valid data. To see what I mean, run the demonstration program, enter a string in the first field, and press Alt+F4 to close the form. Your code won't raise an error, even if the second field doesn't contain a valid number! Fortunately, it doesn't take much to create a generic routine that forces each control on the form to validate itself:
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer) ' You can't close this form without validating all the fields on it. If UnloadMode = vbFormControlMenu Then On Error Resume Next Dim ctrl As Control ' Give the focus to each control on the form, and then ' validate it. For Each ctrl In Controls Err.Clear ctrl.SetFocus If Err = 0 Then ' Don't validate controls that can't receive input focus. ValidateControls If Err = 380 Then ' Validation failed, refuse to close. Cancel = True: Exit Sub End If End If Next End If End Sub |
The CausesValidation property and the Validate event are shared by all the intrinsic controls that are able to get the focus as well as by most external ActiveX controls, even those not specifically written for Visual Basic. This is possible because they are extender features, provided by the Visual Basic runtime to all the controls placed on a form's surface.
TIP
One Visual Basic operator has great potential when it comes time to validate complex strings but is neglected by most Visual Basic developers. Let's say you have a product code that consists of two uppercase characters followed by exactly three digits. You might think that you need some complex string functions to validate such a string until you try the Like operator, as follows:If "AX123" Like "[A-Z][A-Z]###" Then Print "OK"
See Chapter 5 for more information about the Like operator.
Users aren't usually delighted to spend all their time at the keyboard. Your job as a programmer is to make their jobs easier, and so you should strive to streamline their everyday work as much as possible. One way to apply this concept is to provide them with auto-tabbing fields, which are fields that automatically advance users to the next field in the Tab order as soon as they enter a valid value. Most often, auto-tabbing fields are those TextBox controls whose MaxLength property has been assigned a non-null value. Implementing such an auto-tabbing field in Visual Basic is straightforward:
Private Sub Text1_Change() If Len(Text1.Text) = Text1.MaxLength Then SendKeys "{Tab}" End Sub |
The trick, as you see, is to have your program provide the Tab key on behalf of your user. In some cases, this simple approach doesn't work—for example, when you paste a long string into the field. You might want to write code that works around this and other shortcomings. Auto-tabbing is a nice feature but not vital to the application, so whether you write a workaround or not isn't a real problem in most cases.
Many business applications let you enter data in one format and then display it in another. For example, numeric values can be formatted with thousand separators and a fixed number of decimal digits. Currency values might have a $ symbol (or whatever your national currency symbol is) automatically inserted. Phone numbers can be formatted with dashes to split into groups of digits. Credit-card numbers can be made more readable with embedded spaces. Dates can be shown in long-date format ("September 10, 1999"). And so on.
The LostFocus event is an ideal occasion to format the contents of a TextBox control as soon as the input focus leaves it. In most cases, you can perform all your formatting chores using the Format function. For example, you can add thousand separators to a numeric value in the txtNumber control using this code:
Private Sub txtNumber_LostFocus() On Error Resume Next txtNumber.Text = Format(CDbl(txtNumber.Text), _ "#,###,###,##0.######") End Sub |
When the field regains the focus, you'll want to get rid of those thousand separators. You can do it easily using the CDbl function:
Private Sub txtNumber_GotFocus() ' On Error is necessary to account for empty fields. On Error Resume Next txtNumber.Text = CDbl(txtNumber.Text) End Sub |
In some cases, however, formatting and unformatting a value isn't that simple. For example, you can format a Currency value to add parentheses around negative numbers, but there's no built-in Visual Basic function able to return a string formatted in that way to its original condition. Fear not, because nothing prevents you from creating your own formatting and unformatting routines. I have built two general-purpose routines for you to consider.
The FilterString routine filters out all unwanted characters in a string:
Function FilterString(Text As String, validChars As String) As String Dim i As Long, result As String For i = 1 To Len(Text) If InStr(validChars, Mid$(Text, i, 1)) Then result = result & Mid$(Text, i, 1) End If Next FilterString = result End Function |
FilterNumber builds on FilterString to strip down all formatting characters in a number and can also trim trailing decimal zeros:
Function FilterNumber(Text As String, TrimZeros As Boolean) As String Dim decSep As String, i As Long, result As String ' Retrieve the decimal separator symbol. decSep = Format$(0.1, ".") ' Use FilterString for most of the work. result = FilterString(Text, decSep & "-0123456789") ' Do the following only if there is a decimal part and the ' user requested that nonsignificant digits be trimmed. If TrimZeros And InStr(Text, decSep) > 0 Then For i = Len(result) To 1 Step -1 Select Case Mid$(result, i, 1) Case decSep result = Left$(result, i - 1) Exit For Case "0" result = Left$(result, i - 1) Case Else Exit For End Select Next End If FilterNumber = result End Function |
The feature I like most in FilterNumber is that it's locale-independent. It works equally well on both sides of the Atlantic ocean (and on other continents, as well.) Instead of hard-coding the decimal separator character in the code, the routine determines it on the fly, using the Visual Basic for Applications (VBA) Format function. Start thinking internationally now, and you won't have a nervous breakdown when you have to localize your applications in German, French, and Japanese.
TIP
The Format function lets you retrieve many locale-dependent characters and separators.
Format$(0.1, ".") ' Decimal separator Format$(1, ",") ' Thousand separator Mid$(Format(#1/1/99#, "short date"), 2, 1) ' Date separatorYou can also determine whether the system uses dates in "mm/dd/yy" (U.S.) format or "dd/mm/yy" (European) format, using this code:
If Left$(Format$("12/31/1999", "short date"), 2) = 12 Then ' mm/dd/yy format Else ' dd/mm/yyyy format End IfThere's no direct way to determine the currency symbol, but you can derive it by analyzing the result of this function:
Format$(0, "currency") ' Returns "$0.00" in USIt isn't difficult to write a routine that internally uses the information I've just given you to extract the currency symbol as well as its default position (before or after the number) and the default number of decimal digits in currency values. Remember, in some countries the currency symbol is actually a string of two or more characters.
To illustrate these concepts in action, I've built a simple demonstration program that shows how you can format numbers, currency values, dates, phone numbers, and credit-card numbers when exiting a field, and how you can remove that formatting from the result when the input focus reenters the TextBox control. Figure 3-2 shows the formatted results.
Figure 3-2. Formatting and unformatting the contents of TextBox controls makes for more professional-looking applications.
Private Sub txtNumber_GotFocus() ' Filter out nondigit chars and trailing zeros. On Error Resume Next txtNumber.Text = FilterNumber(txtNumber.Text, True) End Sub Private Sub txtNumber_LostFocus() ' Format as a number, grouping thousand digits. On Error Resume Next txtNumber.Text = Format(CDbl(txtNumber.Text), _ "#,###,###,##0.######") End Sub Private Sub txtCurrency_GotFocus() ' Filter out nondigit chars and trailing zeros. ' Restore standard text color. On Error Resume Next txtCurrency.Text = FilterNumber(txtCurrency.Text, True) txtCurrency.ForeColor = vbWindowText End Sub Private Sub txtCurrency_LostFocus() On Error Resume Next ' Show negative values as red text. If CDbl(txtCurrency.Text) < 0 Then txtCurrency.ForeColor = vbRed ' Format currency, but don't use parentheses for negative numbers. ' (FormatCurrency is a new VB6 string function.) txtCurrency.Text = FormatCurrency(txtCurrency.Text, , , vbFalse) End Sub Private Sub txtDate_GotFocus() ' Prepare to edit in short-date format. On Error Resume Next txtDate.Text = Format$(CDate(txtDate.Text), "short date") End Sub Private Sub txtDate_LostFocus() ' Convert to long-date format upon exit. On Error Resume Next txtDate.Text = Format$(CDate(txtDate.Text), "d MMMM yyyy") End Sub Private Sub txtPhone_GotFocus() ' Trim embedded dashes. txtPhone.Text = FilterString(txtPhone.Text, "0123456789") End Sub Private Sub txtPhone_LostFocus() ' Add dashes if necessary. txtPhone.Text = FormatPhoneNumber(txtPhone.Text) End Sub Private Sub txtCreditCard_GotFocus() ' Trim embedded spaces. txtCreditCard.Text = FilterNumber(txtCreditCard.Text, True) End Sub Private Sub txtCreditCard_LostFocus() ' Add spaces if necessary. txtCreditCard.Text = FormatCreditCard(txtCreditCard.Text) End Sub |
Instead of inserting the code that formats phone numbers and credit-card numbers right in the LostFocus event procedures, I built two distinct routines, which can be more easily reused in other applications, as shown in the code below.
Function FormatPhoneNumber(Text As String) As String Dim tmp As String If Text <> "" Then ' First get rid of all embedded dashes, if any. tmp = FilterString(Text, "0123456789") ' Then reinsert them in the correct position. If Len(tmp) <= 7 Then FormatPhoneNumber = Format$(tmp, "!@@@-@@@@") Else FormatPhoneNumber = Format$(tmp, "!@@@-@@@-@@@@") End If End If End Function Function FormatCreditCard(Text As String) As String Dim tmp As String If Text <> "" Then ' First get rid of all embedded spaces, if any. tmp = FilterNumber(Text, False) ' Then reinsert them in the correct position. FormatCreditCard = Format$(tmp, "!@@@@ @@@@ @@@@ @@@@") End If End Function |
Unfortunately, there isn't any way to create locale-independent routines that can format any phone number anywhere in the world. But by grouping all your formatting routines in one module, you can considerably speed up your work if and when it's time to convert your code for another locale. Chapter 5 covers the Format function in greater detail.
You create multiline TextBox controls by setting the MultiLine property to True and the ScrollBars property to 2-Vertical or 3-Both. A vertical scroll bar causes the contents of the control to automatically wrap when a line is too long for the control's width, so this setting is most useful when you're creating memo fields or simple word processor-like programs. If you have both a vertical and a horizontal scroll bar, the TextBox control behaves more like a programmer's editor, and longer lines simply extend beyond the right border. I've never found a decent use for the other settings of the ScrollBars property (0-None and 1-Horizontal) in a multiline TextBox control. Visual Basic ignores the ScrollBars property if MultiLine is False.
Both these properties are read-only at run time, which means that you can't alternate between a regular and a multiline text box, or between a word processor-like multiline field (ScrollBars = 2-Vertical) and an editorlike field (ScrollBars = 3-Both). To tell the whole truth, Visual Basic's support for multiline TextBox controls leaves much to be desired. You can do very little with such controls at run time, except to retrieve and set their Text properties. When you read the contents of a multiline TextBox control, it's up to you to determine where each line of text starts and ends. You do this with a loop that searches for carriage return (CR) and line feed (LF) pairs, or even more easily using the new Split string function:
' Print the lines of text in Text1, labeling them with their line numbers. Dim lines() As String, i As Integer lines() = Split(Text1.Text, vbCrLf) For i = 0 To UBound(lines) Print (i + 1) & ": " & lines(i) Next |
The support offered by Visual Basic for multiline TextBox controls ends here. The language doesn't offer any means for learning such vital information as at which point each line of text wraps, which are the first visible line and the first visible column, which line and column the caret is on, and so on. Moreover, you have no means of programmatically scrolling through a multiline text box. The solutions to these problems require Microsoft Windows API programming, which I'll explain in the Appendix. In my opinion, however, Visual Basic should offer these features as built-in properties and methods.
You should account for two minor issues when including one or more multiline TextBox controls on your forms. When you enter code in a word processor or an editor, you expect that the Enter key will add a newline character (more precisely, a CR-LF character pair) and that the Tab key will insert a tab character and move the caret accordingly. Visual Basic supports these keys, but because both of them have special meaning to Windows the support is limited: The Enter key adds a CR-LF pair only if there isn't a default push button on the form, and the Tab key inserts a tab character only if there aren't other controls on the form whose TabStop property is set to True. In many circumstances, these requirements can't be met, and some of your users will find your user interface annoying. If you can't avoid this problem, at least add a reminder to your users that they can add new lines using the Ctrl+Enter key combination and insert tab characters using the Ctrl+Tab key combination. Another possible approach is to set the TabStop property to False for all the controls in the form in the multiline TextBox's GotFocus event and to restore the original values in the LostFocus event procedure.